package baseAuth

import (
	baseLog "gitee.com/zaiqiang231/go-base-app/base_app/log"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
	"github.com/pkg/errors"
	"golang.org/x/crypto/bcrypt"
	"net/http"
	"strings"
	"time"
)

var (
	ErrFailedAuthentication    = errors.New("token err")
	ErrEmptyAuthHeader         = errors.New("auth header is empty")
	ErrInvalidAuthHeader       = errors.New("auth header is invalid")
	ErrEmptyQueryToken         = errors.New("query token is empty")
	ErrEmptyCookieToken        = errors.New("cookie token is empty")
	ErrEmptyParamToken         = errors.New("parameter token is empty")
	ErrInvalidSigningAlgorithm = errors.New("invalid signing algorithm")
	ErrExpiredToken            = errors.New("token is expired")
)

type JWTAuth struct {
	SigningAlgorithm string //signing algorithm - possible values are HS256, HS384, HS512, RS256, RS384 or RS512
	Key              []byte //Secret key used for signing. Required.
	TokenLookup      string //"header: Authorization, query: token, cookie: jwt"
	TokenHeadName    string //Default value is "Bearer"
	IdentityKey      string
}

type AuthLoginInfo struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

func (jwtAuth *JWTAuth) Unauthorized(c *gin.Context, code int, message string) {
	c.Header("WWW-Authenticate", "Basic realm=JWT")
	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  message,
	})
	c.Abort()
}

func (jwtAuth *JWTAuth) CreateToken(identityKey string, timeout time.Duration) (string, error) {
	token := jwt.New(jwt.GetSigningMethod(jwtAuth.SigningAlgorithm))
	claims := token.Claims.(jwt.MapClaims)
	claims["iss"] = "base-session"
	claims["aud"] = "base-session"
	claims[jwtAuth.IdentityKey] = identityKey
	claims["iat"] = time.Now().Unix()
	claims["exp"] = time.Now().Add(timeout).Unix()
	tokenString, err := token.SignedString(jwtAuth.Key)
	return tokenString, err
}

func (jwtAuth *JWTAuth) GetToken(c *gin.Context) (string, error) {
	var token string
	methods := strings.Split(jwtAuth.TokenLookup, ",")
	for _, method := range methods {
		if len(token) > 0 {
			break
		}
		parts := strings.Split(strings.TrimSpace(method), ":")
		k := strings.TrimSpace(parts[0])
		v := strings.TrimSpace(parts[1])
		switch k {
		case "header":
			token, _ = jwtAuth.jwtFromHeader(c, v)
		case "query":
			token, _ = jwtAuth.jwtFromQuery(c, v)
		case "cookie":
			token, _ = jwtAuth.jwtFromCookie(c, v)
		case "param":
			token, _ = jwtAuth.jwtFromParam(c, v)
		}
	}

	if len(token) <= 0 {
		return "", errors.New("ParseToken failed")
	} else {
		return token, nil
	}
}

func (jwtAuth *JWTAuth) TokenCheck(token string) (bool, string, error) {
	jtoken, err := jwtAuth.parseToken(token)
	if err != nil {
		return false, "", err
	}

	claims := jtoken.Claims.(jwt.MapClaims)
	if claims["exp"] == nil {
		return false, "", errors.New("TokenCheckFunc missing exp field")
	}

	if _, ok := claims["exp"].(float64); !ok {
		return false, "", errors.New("TokenCheckFunc exp must be float64 format")
	}

	if int64(claims["exp"].(float64)) < time.Now().Unix() {
		return false, "", errors.New("TokenCheckFunc token is expired")
	}

	identity := claims[jwtAuth.IdentityKey]

	v, ok := identity.(string)
	if len(v) > 0 && ok {
		baseLog.Debugf("user `%s` is authenticated.", v)
		return true, v, nil
	} else {
		baseLog.Errorf("user authenticated failed.")
		return false, "", errors.New("TokenCheckFunc user authenticated failed.")
	}
}

func (jwtAuth *JWTAuth) TokenCheckFunc(c *gin.Context) {
	token, err := jwtAuth.GetToken(c)
	if err != nil {
		jwtAuth.Unauthorized(c, http.StatusUnauthorized, err.Error())
		return
	}
	_, identity, err := jwtAuth.TokenCheck(token)
	if err != nil {
		jwtAuth.Unauthorized(c, http.StatusUnauthorized, err.Error())
		return
	}
	c.Set(jwtAuth.IdentityKey, identity)
	c.Next()
}

func (jwtAuth *JWTAuth) parseToken(token string) (*jwt.Token, error) {
	return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
		if jwt.GetSigningMethod(jwtAuth.SigningAlgorithm) != t.Method {
			return nil, ErrInvalidSigningAlgorithm
		}
		return jwtAuth.Key, nil
	})
}

func (jwtAuth *JWTAuth) jwtFromHeader(c *gin.Context, key string) (string, error) {
	authHeader := c.Request.Header.Get(key)
	if authHeader == "" {
		return "", ErrEmptyAuthHeader
	}
	parts := strings.SplitN(authHeader, " ", 2)
	if !(len(parts) == 2 && parts[0] == jwtAuth.TokenHeadName) {
		return "", ErrInvalidAuthHeader
	}
	return parts[1], nil
}

func (jwtAuth *JWTAuth) jwtFromQuery(c *gin.Context, key string) (string, error) {
	token := c.Query(key)

	if token == "" {
		return "", ErrEmptyQueryToken
	}

	return token, nil
}

func (jwtAuth *JWTAuth) jwtFromCookie(c *gin.Context, key string) (string, error) {
	cookie, _ := c.Cookie(key)

	if cookie == "" {
		return "", ErrEmptyCookieToken
	}

	return cookie, nil
}

func (jwtAuth *JWTAuth) jwtFromParam(c *gin.Context, key string) (string, error) {
	token := c.Param(key)

	if token == "" {
		return "", ErrEmptyParamToken
	}

	return token, nil
}

// Encrypt 加密
func Encrypt(source string) (string, error) {
	hashedBytes, err := bcrypt.GenerateFromPassword([]byte(source), bcrypt.DefaultCost)
	return string(hashedBytes), err
}

func Compare(hashedPassword, password string) error {
	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
